﻿/*
 * Copyright © 2011 Nokia Corporation. All rights reserved.
 * Nokia and Nokia Connecting People are registered trademarks of Nokia Corporation. 
 * Other product and company names mentioned herein may be trademarks
 * or trade names of their respective owners. 
 * See LICENSE.TXT for license information.
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.ServiceRuntime;
using Microsoft.WindowsAzure.StorageClient;
using IAPRepo_WebRole.Models;
using System.IO;
using System.Diagnostics;
using System.Xml.Serialization;
using System.Net;
using System.Text;
using System.Xml;
using System.Security.Cryptography;

namespace IAPRepo_WebRole.Controllers
{
    public class XmlResult : ActionResult
    {
        public object Data { private get; set; }

        public override void ExecuteResult(ControllerContext context)
        {
            if (this.Data != null)
            {
                var xmlSerializer = new XmlSerializer(this.Data.GetType());
                context.HttpContext.Response.ContentType = "text/xml";
                xmlSerializer.Serialize(context.HttpContext.Response.Output, this.Data);
            }
        }
    }

    public class Application
    {
        public List<Product> Products;
    }

    public class Product
    {
        public String Id;
        public String Image;
        public String Restorable;
    }

    public class ApiController : BaseCloudStorageController
    {
        enum VerificationResult
        {
            OK,
            Failed,
            Refunded,
            InvalidPurchaseTicket
        }

        /// <summary>
        /// GET: /api/<applicationId>
        /// </summary>        
        /// <param name="contentType">Sets response content type to JSON or XML</param>
        /// <param name="account">User's account. If account is not empty products have also restorable attribute</param>
        /// <returns>List of products</returns>
        public ActionResult ListProducts(String applicationId, String contentType, String account)
        {
            if ((contentType ?? String.Empty).Contains("xml"))
            {
                var application = new Application { Products = new List<Product>() };
                if (applicationId == null)
                {
                    return new XmlResult { Data = application };
                }

                var query = tableContext.CreateQuery<IAPProduct>(IAPProduct.TABLE_NAME)
                    .Where(p => p.PartitionKey == IAPProduct.PARTITION_KEY && p.ApplicationId == applicationId)
                    .AsTableServiceQuery();

                foreach (IAPProduct p in query)
                {
                    var product = new Product()
                    {
                        Id = p.ProductId,
                        Image = p.ImageUri,
                    };
                    if (!String.IsNullOrEmpty(account))
                    {
                        product.Restorable = String.IsNullOrEmpty(getPurchaseTicket(applicationId, p.ProductId, account)) ? "no" : "yes";
                    }
                    application.Products.Add(product);
                }

                return new XmlResult { Data = application };
            }
            else
            {
                var results = new { products = new List<Dictionary<string, string>>() };
                if (applicationId == null)
                {
                    return Json(results, JsonRequestBehavior.AllowGet);
                }

                var query = tableContext.CreateQuery<IAPProduct>(IAPProduct.TABLE_NAME)
                    .Where(p => p.PartitionKey == IAPProduct.PARTITION_KEY && p.ApplicationId == applicationId)
                    .AsTableServiceQuery();

                foreach (IAPProduct p in query)
                {
                    var product = new Dictionary<string, string>() {                
                        { "id", p.ProductId },
                        { "image", p.ImageUri }
                    };
                    if (!String.IsNullOrEmpty(account)) 
                    {
                        product.Add("restorable", String.IsNullOrEmpty(getPurchaseTicket(applicationId, p.ProductId, account)) ? "no" : "yes");
                    }
                    results.products.Add(product);
                }

                return Json(results, JsonRequestBehavior.AllowGet);
            }
        }

        /// <summary>
        /// POST: /api/<applicationId>/<productId>
        /// </summary>
        /// <param name="purchaseTicket">Base64 encoded string from JSON POST data: { "purchaseTicket": "PD94bWwgdm...AiLz4=" }</param>
        /// <param name="account">User's account</param>
        /// <returns>
        /// 1. HTTP 200 OK + requested file if purchase ticket is valid.
        /// 2. HTTP 404 Not Found, if resource (appId + productId) is not found 
        /// 3. HTTP 400 [Failed | Refunded | InvalidPurchaseTicket], if purchaseTicket is not accepted
        /// </returns>        
        [HttpPost]
        public ActionResult DownloadProduct(string applicationId, string productId, string purchaseTicket, string account)
        {
            Debug.WriteLine("DownloadProduct, appId=" + applicationId + ", prodId=" + productId + ", purchaseTicket=" + purchaseTicket + ", account=" + account);

            // Attempt to get file by applicationId and productId
            IAPProduct product = getProduct(applicationId, productId);

            // No product found -> return HTTP 404 Not Found
            if (product == null)
            {
                return new HttpNotFoundResult();
            }

            bool newPurchaseTicket = !String.IsNullOrEmpty(purchaseTicket);
            if (!newPurchaseTicket)
            {
                purchaseTicket = getPurchaseTicket(applicationId, productId, account);
            }

            // Validate ticket. In case of failure, return HTTP 400 <reason>
            VerificationResult result = validatePurchaseTicket(applicationId, productId, purchaseTicket);
            if (result == VerificationResult.OK)
            {
                Debug.WriteLine("PurchaseTicket verification PASSED");
                if (newPurchaseTicket)
                {
                    savePurchaseTicket(applicationId, productId, account, purchaseTicket);
                }
            }
            else
            {
                Debug.WriteLine("PurchaseTicket verification FAILED");
                if (result != VerificationResult.Failed && !String.IsNullOrEmpty(purchaseTicket) && !newPurchaseTicket)
                {
                    deletePurchaseTicket(applicationId, productId, account);
                }
                return new HttpStatusCodeResult(400, Enum.GetName(typeof(VerificationResult), result));
            }

            // Everything ok -> send the blob directly to the browser
            return respondWithBinaryBlob(product.DownloadableBlobName);

            // Alternative: use a 302 redirect. This might need more interpretation in the client end.
            // return Redirect(product.DownloadableUri);
        }

        private IAPProduct getProduct(string applicationId, string productId)
        {
            IAPProduct product = null;
            try
            {
                product = tableContext.CreateQuery<IAPProduct>(IAPProduct.TABLE_NAME)
                    .Where(p =>
                        p.PartitionKey == IAPProduct.PARTITION_KEY &&
                        p.ApplicationId == applicationId &&
                        p.ProductId == productId
                    )
                    .Single();
            }
            catch (Exception e)
            {
                Debug.WriteLine(
                    "Product not found, applicationId=" + applicationId + ", " +
                    "productId=" + productId + ": " + e.Message
                );
            }
            return product;
        }

        private string getPurchaseTicket(string applicationId, string productId, string account)
        {
            string purchaseTicket = null;
            if (!String.IsNullOrEmpty(account))
            {
                try
                {
                    purchaseTicket = tableContext.CreateQuery<IAPTicket>(IAPTicket.TABLE_NAME)
                        .Where(p =>
                            p.PartitionKey == IAPTicket.PARTITION_KEY &&
                            p.ApplicationId == applicationId &&
                            p.ProductId == productId &&
                            p.Account == account
                        )
                        .Single().PurchaseTicket;
                }
                catch (Exception e)
                {
                    Debug.WriteLine(
                        "Ticket not found, applicationId=" + applicationId + ", " +
                        "productId=" + productId + ", " +
                        "account=" + account + ": " + e.Message
                    );
                }
            }
            return purchaseTicket;
        }

        private void savePurchaseTicket(string applicationId, string productId, string account, string purchaseTicket)
        {
            if (!String.IsNullOrEmpty(account) && !String.IsNullOrEmpty(purchaseTicket))
            {
                string oldPurchaseTicket = getPurchaseTicket(applicationId, productId, account);
                if (!purchaseTicket.Equals(oldPurchaseTicket))
                {
                    if (oldPurchaseTicket != null)
                    {
                        deletePurchaseTicket(applicationId, productId, account);
                    }
                    IAPTicket ticket = new IAPTicket();
                    ticket.ApplicationId = applicationId;
                    ticket.ProductId = productId;
                    ticket.Account = account;
                    ticket.PurchaseTicket = purchaseTicket;
                    tableContext.AddObject(IAPTicket.TABLE_NAME, ticket);
                    tableContext.SaveChangesWithRetries();
                    Debug.WriteLine("Ticket saved");
                }
            }
        }

        private void deletePurchaseTicket(string applicationId, string productId, string account)
        {
            if (!String.IsNullOrEmpty(account))
            {
                try
                {
                    var ticketQuery = tableContext.CreateQuery<IAPTicket>(IAPTicket.TABLE_NAME)
                        .Where(p =>
                            p.PartitionKey == IAPTicket.PARTITION_KEY &&
                            p.ApplicationId == applicationId &&
                            p.ProductId == productId &&
                            p.Account == account
                        );
                    foreach (var ticket in ticketQuery) 
                    {
                        tableContext.DeleteObject(ticket);
                        Debug.WriteLine("Ticket deleted");
                    }
                    tableContext.SaveChangesWithRetries();
                }
                catch (Exception e)
                {
                    Debug.WriteLine(e);
                }
            }
        }

        /// <summary>
        /// Retrieves the binaries from blob storage
        /// </summary>
        /// <param name="blobName">Binary filename (e.g. 252...acd_binary.png)</param>
        /// <returns>Binaries along http response or null if binaries could not be retrieved</returns>
        private ActionResult respondWithBinaryBlob(String blobName)
        {
            var blobRef = blobContainer.GetBlobReference(blobName);

            try
            {
                Response.ContentType = blobRef.Properties.ContentType;
                Response.AddHeader("content-disposition", "attachment; filename=" + blobName);
                blobRef.DownloadToStream(Response.OutputStream);
                Response.End();
            }
            catch (HttpException e)
            {
                Debug.WriteLine("Could not send file: " + e.Message);
            }

            return null;
        }

        /// <summary>
        /// Validates the purchase ticket first with a smoke test for application id and product id.
        /// Next the purchase ticket gets sent to payment.ovi.com for verification, and in third phase the response is analysed
        /// </summary>
        private VerificationResult validatePurchaseTicket(string applicationId, string productId, string purchaseTicket)
        {
            if (prevalidatePurchaseTicket(applicationId, productId, purchaseTicket))
            {
                XmlDocument response = postForVerification(purchaseTicket);
                return checkVerificationResponse(response);
            }
            return VerificationResult.InvalidPurchaseTicket;
        }

        /// <summary>
        /// Decodes the purchase ticket and verifies, that the purchase ticket matches to the requested downloadable
        /// </summary>        
        private bool prevalidatePurchaseTicket(string applicationId, string productId, string purchaseTicket)
        {
            if (purchaseTicket != null && purchaseTicket.Length > 0)
            {
                try
                {
                    // Decode ticket 
                    byte[] ticket = Convert.FromBase64String(purchaseTicket);
                    XmlDocument doc = new XmlDocument();
                    doc.LoadXml(Encoding.UTF8.GetString(ticket));

                    XmlElement ticketNode = (XmlElement)doc.GetElementsByTagName("PurchaseTicket").Item(0);
                    string appId = ticketNode.GetAttribute("applicationId");
                    string prodId = ticketNode.GetAttribute("productId");

                    // TODO: check also applicationId after the client applications start using correct IDs
                    //if (appId.Equals(applicationId) && prodId.Equals(productId))
                    if (prodId.Equals(productId) || prodId.Equals("675193")) // Allow also the IAP test purchase ticket for demonstration
                    {
                        return true;
                    }
                }
                catch (Exception e) { }
            }
            return false;
        }

        /// <summary>
        /// Sends the purchase ticket for verification to payment.ovi.com
        /// </summary>
        /// <param name="purchaseTicket">Base64 encoded string</param>
        /// <returns>Xml document containing verification response from payment.ovi.com</returns>
        private XmlDocument postForVerification(string purchaseTicket)
        {
            string url = "https://payment.ovi.com/iap/1.0/purchases/verify?method=GET";
            string xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"
                        + "<PurchaseVerificationRequest xmlns=\"http://payment.ovi.com/iap\"><Binary>"
                        + purchaseTicket
                        + "</Binary></PurchaseVerificationRequest>";

            XmlDocument responseXml = new XmlDocument();
            HttpWebResponse res = null;

            try
            {
                // Build a request
                HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url);
                byte[] buffer = Encoding.UTF8.GetBytes("content=" + HttpUtility.UrlEncode(xml));

                req.Method = "POST";
                req.ContentType = "application/x-www-form-urlencoded; charset=UTF-8";
                req.ContentLength = buffer.Length;

                // Execute the request
                Stream reqst = req.GetRequestStream();
                reqst.Write(buffer, 0, buffer.Length);
                reqst.Flush();
                reqst.Close();

                res = (HttpWebResponse)req.GetResponse();
            }
            catch (WebException we)
            {
                // Server responses in the range of 4xx and 5xx throw a WebException
                res = ((HttpWebResponse)we.Response);
                Debug.WriteLine("Purchase verification request failed: " + res.StatusCode + " " + res.StatusDescription);
            }

            if (res != null)
            {
                try
                {
                    using (Stream stream = res.GetResponseStream())
                    {
                        /* Debug response
                            string xmlStr = new StreamReader(stream).ReadToEnd();
                            responseXml.LoadXml(xmlStr);
                            Debug.WriteLine("Verification response: " + xmlStr);
                        */

                        XmlTextReader xmlReader = new XmlTextReader(stream);
                        responseXml.Load(xmlReader);
                    }
                }
                catch (XmlException e)
                {
                    return null;
                }
                finally
                {
                    ((IDisposable)res).Dispose();
                }
            }
            return responseXml;
        }


        /// <summary>
        /// Checks the verification result value from purchase verification xml
        /// </summary>
        /// <param name="xml">Xml containing PurchaseVerificationResponse element</param>        
        private VerificationResult checkVerificationResponse(XmlDocument xml)
        {
            if (xml != null)
            {
                XmlElement root = xml.DocumentElement;
                if (root != null && root.Name.Equals("PurchaseVerificationResponse"))
                {
                    try
                    {
                        // Convert result attribute to VerificationResult
                        return (VerificationResult)Enum.Parse(typeof(VerificationResult), root.GetAttribute("result"));
                    }
                    catch (Exception e) { }
                }
            }
            return VerificationResult.Failed;
        }
    }
}
